{% load i18n %}
{% load custom_translation %}

<link type="text/css" href="/static/css/tooltips.css" rel="stylesheet" />
<link type="text/css" href="/static/css/elo-graph.css" rel="stylesheet" />
<script type="text/javascript" src="/static/js/d3.v6.min.js"></script>

{% url 'networks-for-elo-list' as networks_for_elo_list_url %}
<script>
var api_url = "{{networks_for_elo_list_url|escapejs}}";
var run_name = "{{run.name|escapejs}}";

// A reasonable number of data rows to use to compute a 'small' spacing or buffer for the X axis of data rows.
const DATAROW_BUFFER_SCALE = 10000;

function makeGraph() {
  d3.json(api_url + "?run__name=" + run_name).then(function(networks) {
    // Extract unique network sizes
    var networkSizes = new Set(networks.map(network => network["network_size"]));
    // Natural numeric sort
    var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
    var compare = function(a,b) {
      aIsRandom = a.toLowerCase() == "random";
      bIsRandom = b.toLowerCase() == "random";
      if(aIsRandom && bIsRandom)
        return 0;
      if(aIsRandom)
        return -1;
      if(bIsRandom)
        return 1;
      return collator.compare(a,b);
    };
    networkSizes = Array.from(networkSizes).sort(compare);

    var eloPerLogGamma = Math.log10(Math.E) * 400.0;
    for(var i = 0; i<networks.length; i++) {
      var network = networks[i];
      network["time"] = new Date(network["created_at"]);
      network["elo"] = network["log_gamma"] * eloPerLogGamma;
      network["elostdev"] = network["log_gamma_uncertainty"] * eloPerLogGamma;
    }
    var minCreatedAt = d3.min(networks.map(network => network["time"]));
    var maxCreatedAt = d3.max(networks.map(network => network["time"]));
    var minDataRows = d3.min(networks.filter(network => !!network["total_num_data_rows"]).map(network => network["total_num_data_rows"]));
    var maxDataRows = d3.max(networks.filter(network => !!network["total_num_data_rows"]).map(network => network["total_num_data_rows"]));
    if(!minDataRows)
      minDataRows = 0;
    if(!maxDataRows)
      maxDataRows = DATAROW_BUFFER_SCALE;

    var eloGraphDiv = d3.select("#eloGraph");

    // These are internal coordinates to the SVG - the actual size is scaled to whatever
    // size can fit on the user's browser, through CSS magic.
    var wholeGraphWidth = 800;
    var wholeGraphHeight = 540;

    var svg =
        eloGraphDiv
        .append("div")
        .classed("svg-container", true)
        .append("svg")
        .attr("preserveAspectRatio", "xMinYMin meet")
        .attr("viewBox", "0 0 " + wholeGraphWidth + " " + wholeGraphHeight)
        .classed("svg-content-responsive", true);

    // Set aside space for axes
    var axisMargin = {top: 10, right: 30, bottom: 30, left: 60};
    // The space of the graph to plot stuff, after taking out the axes
    var graphSpace =
        svg
        .append("g")
        .attr("transform","translate(" + axisMargin.left + "," + axisMargin.top + ")");
    var graphSpaceWidth = wholeGraphWidth - axisMargin.left - axisMargin.right;
    var graphSpaceHeight = wholeGraphHeight - axisMargin.top - axisMargin.bottom;

    // X axis
    var xScale = null;
    var xScaleIsTime = false;
    var xScaleIsDataRows = false;
    var xScaleIsLogDataRows = false;
    var xDomainMin = null;
    var xDomainMax = null;
    var xAxis = d3.axisBottom(d3.scaleLinear()).tickSize(-graphSpaceHeight*1.0);

    //Functions to switch between different x axis scalings
    var setXScaleAsTime = function() {
      xScaleIsTime = true;
      xScaleIsDataRows = false;
      xScaleIsLogDataRows = false;

      var buffer = 0.01 * (maxCreatedAt.getTime() - minCreatedAt.getTime());
      buffer += 1000 * 60 * 30; //30 minutes in milliseconds
      xDomainMin = new Date(minCreatedAt.getTime()-buffer);
      xDomainMax = new Date(maxCreatedAt.getTime()+buffer*2);
      xScale =
        d3.scaleTime()
        .domain([xDomainMin, xDomainMax])
        .range([0, graphSpaceWidth]);
      xAxis.scale(xScale).ticks(10);
      d3.selectAll(".eloGraphButton").classed("eloButtonHighlighted", false);
      d3.select("#eloGraphTimeButton").classed("eloButtonHighlighted", true);
    };
    var setXScaleAsDataRows = function() {
      xScaleIsTime = false;
      xScaleIsDataRows = true;
      xScaleIsLogDataRows = false;

      var buffer = 0.01 * (maxDataRows - minDataRows);
      buffer += DATAROW_BUFFER_SCALE;
      xDomainMin = minDataRows-buffer;
      xDomainMax = maxDataRows+buffer*2;
      xScale =
        d3.scaleLinear()
        .domain([xDomainMin, xDomainMax])
        .range([0, graphSpaceWidth]);
      xAxis.scale(xScale).ticks(10,"~s");
      d3.selectAll(".eloGraphButton").classed("eloButtonHighlighted", false);
      d3.select("#eloGraphDataRowsButton").classed("eloButtonHighlighted", true);
    };
    var setXScaleAsDataRowsLog = function() {
      xScaleIsTime = false;
      xScaleIsDataRows = false;
      xScaleIsLogDataRows = true;

      var maxDataRowsLowerBounded = Math.max(maxDataRows,DATAROW_BUFFER_SCALE);
      var buffer = 0.2 * (maxDataRows - minDataRows);
      buffer += DATAROW_BUFFER_SCALE;
      xDomainMin = Math.min(minDataRows + DATAROW_BUFFER_SCALE * 10, maxDataRowsLowerBounded * 0.02);
      xDomainMax = maxDataRowsLowerBounded + buffer;
      xScale =
        d3.scaleLog()
        .domain([xDomainMin, xDomainMax])
        .range([0, graphSpaceWidth]);
      xAxis.scale(xScale).ticks(15,"~s");
      d3.selectAll(".eloGraphButton").classed("eloButtonHighlighted", false);
      d3.select("#eloGraphDataRowsLogButton").classed("eloButtonHighlighted", true);
    };
    var setXScaleAsDataRowsLogRecent = function() {
      xScaleIsTime = false;
      xScaleIsDataRows = false;
      xScaleIsLogDataRows = true;

      var maxDataRowsLowerBounded = Math.max(maxDataRows,DATAROW_BUFFER_SCALE);
      var buffer = 0.05 * (maxDataRows - minDataRows);
      buffer += DATAROW_BUFFER_SCALE;
      xDomainMin = maxDataRowsLowerBounded * 0.65;
      xDomainMax = maxDataRowsLowerBounded + buffer;
      xScale =
        d3.scaleLog()
        .domain([xDomainMin, xDomainMax])
        .range([0, graphSpaceWidth]);
      xAxis.scale(xScale).ticks(15,"~s");
      d3.selectAll(".eloGraphButton").classed("eloButtonHighlighted", false);
      d3.select("#eloGraphDataRowsLogRecentButton").classed("eloButtonHighlighted", true);
    };

    setXScaleAsDataRowsLogRecent();

    var xAxisContent = graphSpace.append("g").attr("transform", "translate(0," + graphSpaceHeight + ")");
    xAxis(xAxisContent);

    // Takes the xDomain and finds a reasonable y elo range to capture all networks that fall within that range of time.
    var computeYDomainFromXDomain = function(xDomain) {
      var filtered;
      if(xScaleIsTime)
        filtered = networks.filter(network => network["time"] >= xDomain[0] && network["time"] <= xDomain[1]);
      else
        filtered = networks.filter(network => network["total_num_data_rows"] >= xDomain[0] && network["total_num_data_rows"] <= xDomain[1]);

      var minElo = d3.min(filtered.map(network => network["elo"] - Math.min(300, 2.5 * network["elostdev"])));
      var maxElo = d3.max(filtered.map(network => network["elo"] + Math.min(300, 2.5 * network["elostdev"])));
      // No elements
      if(minElo === undefined || maxElo === undefined) {
        minElo = 0;
        maxElo = 100;
      }
      var eloBuffer = 0.01 * (maxElo - minElo) + 10;
      minElo = minElo - eloBuffer;
      maxElo = maxElo + eloBuffer;
      //Also stretch out a little by scale
      if(maxElo > 0)
        maxElo *= 1.01;
      if(minElo > 0)
        minElo *= 0.99;

      return [minElo, maxElo];
    };

    // Y axis
    var yScale =
        d3.scaleLinear()
        .domain(computeYDomainFromXDomain(xScale.domain()))
        .range([graphSpaceHeight, 0]);
    var yAxis = d3.axisLeft(yScale).tickSize(-graphSpaceWidth*1.0).ticks(20);
    var yAxisContent = graphSpace.append("g");
    yAxis(yAxisContent);
    var unzoomedYDomainMin = yScale.domain()[0];
    var unzoomedYDomainMax = yScale.domain()[1];

    // Colors
    var colorScale = d3.scaleSequential(d3.interpolateWarm).domain([0,networkSizes.length-1]);
    var colorDict = {};
    var uncertaintyBarColorDict = {};
    for(var i = 0; i<networkSizes.length; i++) {
      var color = colorScale(networkSizes.length-i-1);
      colorDict[networkSizes[i]] = color;
      uncertaintyBarColorDict[networkSizes[i]] = d3.color(color).brighter(0.4).copy({opacity:0.8});
    }

    var escapeHtml = function(s) {
      return s
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    };

    // Tooltip on hover
    var body = d3.select("body");
    var tooltip =
        body
        .append("div")
        .classed("eloGraphTooltip",true)
        .style("visibility", "hidden");
    // Translate the tooltip to be just to the lower right of the mouse position of the event
    var translateToolTip = function(event) {
      // We set it relative to the body because the tooltip div is appended to the body div
      // We append it to the body div so that the tooltip automatically resizes and wraps if it would
      // overflow body boundaries
      var bodyRect = body.node().getBoundingClientRect();
      tooltip
        .style("left", (event.clientX - bodyRect.left + 20) + "px")
        .style("top", (event.clientY - bodyRect.top + 20) + "px")
    };
    var toolTipMouseOver = function(event,network) {
      tooltip
        .html(escapeHtml(network["name"]) + "<br>" +
              escapeHtml(network["created_at"]) + "<br>" +
              "Elo " + network["elo"].toFixed(1) + " +/- " + (2 * network["elostdev"]).toFixed(1))
        .style("visibility", "visible");
      translateToolTip(event);
    };
    var toolTipMouseMove = function(event,network) {
      translateToolTip(event);
    };
    var toolTipMouseLeave = function() {
      tooltip
        .style("visibility", "hidden");
    };

    // Add a clipPath, so that things outside this area won't be drawn.
    var clip =
        graphSpace
        .append("defs")
        .append("svg:clipPath")
        .attr("id", "clip")
        .append("svg:rect")
        .attr("width", graphSpaceWidth)
        .attr("height", graphSpaceHeight)
        .attr("x", 0)
        .attr("y", 0);

    // Create a subelement that holds the data alone, clipped
    var dataSpace =
        graphSpace.append('g')
        .attr("clip-path", "url(#clip)");

    // Declare function now, it gets set later below
    var rerenderDataAfterScaling = null;

    // Handle brushing for zooming
    var justAfterBrushing = false;
    var recentlyClicked = false;
    function noLongerRecentlyClicked() { recentlyClicked = false; }

    // Translate a brushing result into a zoom
    var updateChartForBrush = function(event) {
      if(justAfterBrushing)
        return false;
      extent = event.selection;
      if(!extent) {
        if(!recentlyClicked) {
          recentlyClicked = true;
          setTimeout(noLongerRecentlyClicked, 300);
          return false;
        }
        //Upon a double-click with at most 300ms of tolerance, restore the original zoom
        var newDomain = [xDomainMin, xDomainMax];
        xScale.domain(newDomain);
        yScale.domain(computeYDomainFromXDomain(newDomain));
      }
      else {
        //Do nothing on zero or negative extent
        if(extent[1] <= extent[0])
          return false;

        var newDomain = [xScale.invert(extent[0]), xScale.invert(extent[1])];
        xScale.domain(newDomain);
        yScale.domain(computeYDomainFromXDomain(newDomain));
        // Remove the brush instead of letting it continue to display
        // Also the act of doing this will cause updateChartForBrush to be recursively called
        // back into, so suppress the effects of this with justAfterBrushing so that the recursive
        // calls return immediately.
        justAfterBrushing = true;
        dataSpace.select(".brush").call(brush.move, null)
        justAfterBrushing = false;
      }

      // Re-render now that the zoom is applied to the scales
      rerenderDataAfterScaling(200);
    };

    // Add brushing for zooming. We add brush FIRST so that the data points are on top and can be hovered
    var brush =
        d3.brushX()
        .extent([[0,0],[graphSpaceWidth,graphSpaceHeight]])
        .on("end", updateChartForBrush);
    dataSpace
      .append("g")
      .attr("class", "brush")
      .call(brush);

    // Now add data points
    var baseDataRadius = 2;
    var baseStrokeWidth = 1.2;
    var largeRadiusBonus = 2;
    var largeStrokeWidthBonus = 1.5;

    var curDataRadius = baseDataRadius;
    var curStrokeWidth = baseStrokeWidth;
    dataSpace.append('g')
      .selectAll("line")
      .data(networks)
      .enter()
      .append("line")
      .attr("class", network => "dataUncertainty " + "dataUncertainty_" + network["network_size"])
      .attr("stroke-width", curStrokeWidth)
      .style("stroke", network => uncertaintyBarColorDict[network["network_size"]])
      .on("mouseover", toolTipMouseOver)
      .on("mousemove", toolTipMouseMove)
      .on("mouseleave", toolTipMouseLeave);
    dataSpace.append('g')
      .selectAll("circle")
      .data(networks)
      .enter()
      .append("circle")
      .attr("r", curDataRadius)
      .attr("class", network => "dataPoint " + "dataPoint_" + network["network_size"])
      .style("fill", network => colorDict[network["network_size"]])
      .on("mouseover", toolTipMouseOver)
      .on("mousemove", toolTipMouseMove)
      .on("mouseleave", toolTipMouseLeave);

    // Renders the data and axes
    rerenderDataAfterScaling = function(transitionDuration) {
      xAxis(xAxisContent.transition().duration(transitionDuration));
      yAxis(yAxisContent.transition().duration(transitionDuration));

      // Compute a ratio with which to scale the line thicknesses and such as we zoom in.
      var xRatio = (xScale.domain()[1] - xScale.domain()[0]) / (xDomainMax - xDomainMin);
      var yRatio = (yScale.domain()[1] - yScale.domain()[0]) / (unzoomedYDomainMax - unzoomedYDomainMin);
      var ratio = Math.max(xRatio,yRatio);
      // Also scale ratio based on the abs number of data points
      ratio /= Math.max(1.0, Math.sqrt(200.0 / (5.0 + networks.length)));

      curStrokeWidth = baseStrokeWidth / Math.max(0.2, Math.sqrt(ratio));
      curDataRadius = baseDataRadius / Math.max(0.2, Math.sqrt(ratio));

      var lines = dataSpace.selectAll("line");
      var circles = dataSpace.selectAll("circle");
      if(transitionDuration > 0) {
        lines = lines.transition().duration(transitionDuration);
        circles = circles.transition().duration(transitionDuration);
      }
      if(xScaleIsTime) {
        lines
          .attr("x1", network => xScale(network["time"]) )
          .attr("y1", network => yScale(network["elo"] - 2.0 * network["elostdev"]))
          .attr("x2", network => xScale(network["time"]) )
          .attr("y2", network => yScale(network["elo"] + 2.0 * network["elostdev"]))
          .attr("stroke-width",curStrokeWidth)
          .attr("visibility", "visible");
        circles
          .attr("cx", network => xScale(network["time"]))
          .attr("cy", network => yScale(network["elo"]))
          .attr("r",curDataRadius)
          .attr("visibility", "visible");
      }
      else if(xScaleIsDataRows) {
        lines
          .attr("x1", network => xScale(network["total_num_data_rows"] ? network["total_num_data_rows"] : 0))
          .attr("y1", network => yScale(network["elo"] - 2.0 * network["elostdev"]))
          .attr("x2", network => xScale(network["total_num_data_rows"] ? network["total_num_data_rows"] : 0))
          .attr("y2", network => yScale(network["elo"] + 2.0 * network["elostdev"]))
          .attr("stroke-width",curStrokeWidth)
          .attr("visibility", network => network["total_num_data_rows"] ? "visible" : "hidden");
        circles
          .attr("cx", network => xScale(network["total_num_data_rows"] ? network["total_num_data_rows"] : 0))
          .attr("cy", network => yScale(network["elo"]))
          .attr("r",curDataRadius)
          .attr("visibility", network => network["total_num_data_rows"] ? "visible" : "hidden");
      }
      else if(xScaleIsLogDataRows) {
        lines
          .attr("x1", network => xScale(network["total_num_data_rows"] > 1 ? network["total_num_data_rows"] : 1))
          .attr("y1", network => yScale(network["elo"] - 2.0 * network["elostdev"]))
          .attr("x2", network => xScale(network["total_num_data_rows"] > 1 ? network["total_num_data_rows"] : 1))
          .attr("y2", network => yScale(network["elo"] + 2.0 * network["elostdev"]))
          .attr("stroke-width",curStrokeWidth)
          .attr("visibility", network => network["total_num_data_rows"] > 1 ? "visible" : "hidden");
        circles
          .attr("cx", network => xScale(network["total_num_data_rows"] ? network["total_num_data_rows"] : 1))
          .attr("cy", network => yScale(network["elo"]))
          .attr("r",curDataRadius)
          .attr("visibility", network => network["total_num_data_rows"] > 1? "visible" : "hidden");
      }
    };
    rerenderDataAfterScaling(0);

    var postProcessXScaleChanging = function() {
      xAxis.scale(xScale);
      yScale.domain(computeYDomainFromXDomain(xScale.domain()));
      unzoomedYDomainMin = yScale.domain()[0];
      unzoomedYDomainMax = yScale.domain()[1];
      rerenderDataAfterScaling(0);
    };

    // Enable buttons
    d3.select("#eloGraphTimeButton").on("click", function () {
      setXScaleAsTime();
      postProcessXScaleChanging();
    });
    d3.select("#eloGraphDataRowsButton").on("click", function () {
      setXScaleAsDataRows();
      postProcessXScaleChanging();
    });
    d3.select("#eloGraphDataRowsLogButton").on("click", function () {
      setXScaleAsDataRowsLog();
      postProcessXScaleChanging();
    });
    d3.select("#eloGraphDataRowsLogRecentButton").on("click", function () {
      setXScaleAsDataRowsLogRecent();
      postProcessXScaleChanging();
    });

    //Add legend
    graphSpace
      .selectAll(".eloLegend")
      .data(networkSizes)
      .enter()
      .append("g")
      .classed("eloLegend",true)
      .append("text")
      .attr("x", graphSpaceWidth * 0.85)
      .attr("y", function(networkSize,i) { return graphSpaceHeight * 0.65 + i * 20; })
      .text(networkSize => networkSize)
      .style("fill", networkSize => colorDict[networkSize])
      .style("font-size", 15)
      .on("mouseover", function(mouseEvent,networkSize) {
        d3.selectAll(".dataUncertainty_" + networkSize).attr("stroke-width",curStrokeWidth + largeStrokeWidthBonus);
        d3.selectAll(".dataPoint_" + networkSize).attr("r",curDataRadius + largeRadiusBonus);
      })
      .on("mouseleave", function(mouseEvent,networkSize) {
        d3.selectAll(".dataUncertainty_" + networkSize).attr("stroke-width",curStrokeWidth);
        d3.selectAll(".dataPoint_" + networkSize).attr("r",curDataRadius);
      });

  });
}

document.addEventListener("DOMContentLoaded", makeGraph);

</script>
